package pl.vanta.mjuzik2.impl.player.engine;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Control;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;

import javazoom.spi.PropertiesContainer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.tritonus.share.sampled.TAudioFormat;
import org.tritonus.share.sampled.file.TAudioFileFormat;

import pl.vanta.mjuzik2.impl.player.PlayerEvent;
import pl.vanta.mjuzik2.impl.player.PlayerStatus;

public class BasicPlayer implements BasicController, Runnable
{
	public static int						EXTERNAL_BUFFER_SIZE	= 4000 * 4;
	public static int						SKIP_INACCURACY_SIZE	= 1200;
	protected Thread						thread					= null;
	protected Object						dataSource;
	protected AudioInputStream				encodedAudioInputStream;
	protected int							encodedLength			= -1;
	protected AudioInputStream				audioInputStream;
	protected AudioFileFormat				audioFileFormat;
	protected SourceDataLine				sourceDataLine;
	protected FloatControl					gainControl;
	protected FloatControl					panControl;
	protected String						mixerName				= null;
	private int								lineCurrentBufferSize	= -1;
	private int								lineBufferSize			= -1;
	private long							threadSleep				= -1;
	private static Log						log						= LogFactory.getLog(BasicPlayer.class);

	/**
	 * These variables are used to distinguish stopped, paused, playing states.
	 * We need them to control Thread.
	 */

	private PlayerStatus					status					= PlayerStatus.UNKNOWN;

	private Collection<BasicPlayerListener>	listeners;
	private Map<String, Object>				emptyMap				= new HashMap<String, Object>();

	/**
	 * Constructs a Basic Player.
	 */
	public BasicPlayer()
	{
		this.dataSource = null;
		this.listeners = new ArrayList<BasicPlayerListener>();

		this.emptyMap = new HashMap<String, Object>();

		reset();
	}

	protected void reset()
	{
		this.status = PlayerStatus.UNKNOWN;
		if (this.audioInputStream != null)
		{
			synchronized (this.audioInputStream)
			{
				closeStream();
			}
		}
		this.audioInputStream = null;
		this.audioFileFormat = null;
		this.encodedAudioInputStream = null;
		this.encodedLength = -1;
		if (this.sourceDataLine != null)
		{
			this.sourceDataLine.stop();
			this.sourceDataLine.close();
			this.sourceDataLine = null;
		}
		this.gainControl = null;
		this.panControl = null;
	}

	/**
	 * Add listener to be notified.
	 * 
	 * @param bpl
	 */
	public void addBasicPlayerListener(BasicPlayerListener bpl)
	{
		this.listeners.add(bpl);
	}

	/**
	 * Return registered listeners.
	 * 
	 * @return
	 */
	public Collection<BasicPlayerListener> getListeners()
	{
		return this.listeners;
	}

	/**
	 * Remove registered listener.
	 * 
	 * @param bpl
	 */
	public void removeBasicPlayerListener(BasicPlayerListener bpl)
	{
		if (this.listeners != null)
		{
			this.listeners.remove(bpl);
		}
	}

	/**
	 * Set SourceDataLine buffer size. It affects audio latency. (the delay
	 * between line.write(data) and real sound). Minimum value should be over
	 * 10000 bytes.
	 * 
	 * @param size
	 *            -1 means maximum buffer size available.
	 */
	public void setLineBufferSize(int size)
	{
		this.lineBufferSize = size;
	}

	/**
	 * Return SourceDataLine buffer size.
	 * 
	 * @return -1 maximum buffer size.
	 */
	public int getLineBufferSize()
	{
		return this.lineBufferSize;
	}

	/**
	 * Return SourceDataLine current buffer size.
	 * 
	 * @return
	 */
	public int getLineCurrentBufferSize()
	{
		return this.lineCurrentBufferSize;
	}

	/**
	 * Set thread sleep time. Default is -1 (no sleep time).
	 * 
	 * @param time
	 *            in milliseconds.
	 */
	public void setSleepTime(long time)
	{
		this.threadSleep = time;
	}

	/**
	 * Return thread sleep time in milliseconds.
	 * 
	 * @return -1 means no sleep time.
	 */
	public long getSleepTime()
	{
		return this.threadSleep;
	}

	/**
	 * Returns BasicPlayer status.
	 * 
	 * @return status
	 */
	public PlayerStatus getStatus()
	{
		return this.status;
	}

	/**
	 * Open file to play.
	 */
	public void open(File file) throws BasicPlayerException
	{
		log.info("open(" + file + ")");
		if (file != null)
		{
			this.dataSource = file;
			initAudioInputStream();
		}
	}

	/**
	 * Open URL to play.
	 */
	public void open(URL url) throws BasicPlayerException
	{
		log.info("open(" + url + ")");
		if (url != null)
		{
			this.dataSource = url;
			initAudioInputStream();
		}
	}

	/**
	 * Open inputstream to play.
	 */
	public void open(InputStream inputStream) throws BasicPlayerException
	{
		log.info("open(" + inputStream + ")");
		if (inputStream != null)
		{
			this.dataSource = inputStream;
			initAudioInputStream();
		}
	}

	/**
	 * Inits AudioInputStream and AudioFileFormat from the data source.
	 * 
	 * @throws BasicPlayerException
	 */
	@SuppressWarnings("unchecked")
	protected void initAudioInputStream() throws BasicPlayerException
	{
		try
		{
			reset();
			notifyEvent(PlayerEvent.OPENING, getEncodedStreamPosition(), -1, this.dataSource);

			if (this.dataSource instanceof URL)
			{
				initAudioInputStream((URL) this.dataSource);
			}
			else if (this.dataSource instanceof File)
			{
				initAudioInputStream((File) this.dataSource);
			}
			else if (this.dataSource instanceof InputStream)
			{
				initAudioInputStream((InputStream) this.dataSource);
			}

			createLine();

			// Notify listeners with AudioFileFormat properties.
			Map<String, Object> properties = null;

			if (this.audioFileFormat instanceof TAudioFileFormat)
			{
				// Tritonus SPI compliant audio file format.
				properties = ((TAudioFileFormat) this.audioFileFormat).properties();
				// Clone the Map because it is not mutable.
				properties = new HashMap<String, Object>(properties);
			}
			else
			{
				properties = new HashMap<String, Object>();
			}

			// Add JavaSound properties.
			if (this.audioFileFormat.getByteLength() > 0)
				properties.put("audio.length.bytes", new Integer(this.audioFileFormat.getByteLength()));
			if (this.audioFileFormat.getFrameLength() > 0)
				properties.put("audio.length.frames", new Integer(this.audioFileFormat.getFrameLength()));
			if (this.audioFileFormat.getType() != null)
				properties.put("audio.type", (this.audioFileFormat.getType().toString()));

			// Audio format.
			AudioFormat audioFormat = this.audioFileFormat.getFormat();
			if (audioFormat.getFrameRate() > 0)
				properties.put("audio.framerate.fps", new Float(audioFormat.getFrameRate()));
			if (audioFormat.getFrameSize() > 0)
				properties.put("audio.framesize.bytes", new Integer(audioFormat.getFrameSize()));
			if (audioFormat.getSampleRate() > 0)
				properties.put("audio.samplerate.hz", new Float(audioFormat.getSampleRate()));
			if (audioFormat.getSampleSizeInBits() > 0)
				properties.put("audio.samplesize.bits", new Integer(audioFormat.getSampleSizeInBits()));
			if (audioFormat.getChannels() > 0)
				properties.put("audio.channels", new Integer(audioFormat.getChannels()));

			if (audioFormat instanceof TAudioFormat)
			{
				// Tritonus SPI compliant audio format.
				Map addproperties = ((TAudioFormat) audioFormat).properties();
				properties.putAll(addproperties);
			}

			// Add SourceDataLine
			properties.put("basicplayer.sourcedataline", this.sourceDataLine);

			this.status = PlayerStatus.OPENED;

			for (BasicPlayerListener bpl : this.listeners)
			{
				bpl.opened(this.dataSource, properties);
			}

			notifyEvent(PlayerEvent.OPENED, getEncodedStreamPosition(), -1, null);
		}
		catch (LineUnavailableException e)
		{
			throw new BasicPlayerException(e);
		}
		catch (UnsupportedAudioFileException e)
		{
			throw new BasicPlayerException(e);
		}
		catch (IOException e)
		{
			throw new BasicPlayerException(e);
		}
	}

	/**
	 * Inits Audio ressources from file.
	 */
	protected void initAudioInputStream(File file) throws UnsupportedAudioFileException, IOException
	{
		this.audioInputStream = AudioSystem.getAudioInputStream(file);
		this.audioFileFormat = AudioSystem.getAudioFileFormat(file);
	}

	/**
	 * Inits Audio ressources from URL.
	 */
	protected void initAudioInputStream(URL url) throws UnsupportedAudioFileException, IOException
	{
		this.audioInputStream = AudioSystem.getAudioInputStream(url);
		this.audioFileFormat = AudioSystem.getAudioFileFormat(url);
	}

	/**
	 * Inits Audio ressources from InputStream.
	 */
	protected void initAudioInputStream(InputStream inputStream) throws UnsupportedAudioFileException, IOException
	{
		this.audioInputStream = AudioSystem.getAudioInputStream(inputStream);
		this.audioFileFormat = AudioSystem.getAudioFileFormat(inputStream);
	}

	/**
	 * Inits Audio ressources from AudioSystem.<br>
	 */
	protected void initLine() throws LineUnavailableException
	{
		log.info("initLine()");
		if (this.sourceDataLine == null)
			createLine();
		if (!this.sourceDataLine.isOpen())
		{
			openLine();
		}
		else
		{
			AudioFormat lineAudioFormat = this.sourceDataLine.getFormat();
			AudioFormat audioInputStreamFormat = this.audioInputStream == null ? null : this.audioInputStream
					.getFormat();
			if (!lineAudioFormat.equals(audioInputStreamFormat))
			{
				this.sourceDataLine.close();
				openLine();
			}
		}
	}

	/**
	 * Inits a DateLine.<br>
	 * 
	 * We check if the line supports Gain and Pan controls.
	 * 
	 * From the AudioInputStream, i.e. from the sound file, we fetch information
	 * about the format of the audio data. These information include the
	 * sampling frequency, the number of channels and the size of the samples.
	 * There information are needed to ask JavaSound for a suitable output line
	 * for this audio file. Furthermore, we have to give JavaSound a hint about
	 * how big the internal buffer for the line should be. Here, we say
	 * AudioSystem.NOT_SPECIFIED, signaling that we don't care about the exact
	 * size. JavaSound will use some default value for the buffer size.
	 */
	protected void createLine() throws LineUnavailableException
	{
		log.info("Create Line");
		if (this.sourceDataLine == null)
		{
			AudioFormat sourceFormat = this.audioInputStream.getFormat();
			log.info("Create Line : Source format : " + sourceFormat.toString());
			int nSampleSizeInBits = sourceFormat.getSampleSizeInBits();
			if (nSampleSizeInBits <= 0)
				nSampleSizeInBits = 16;
			if ((sourceFormat.getEncoding() == AudioFormat.Encoding.ULAW)
					|| (sourceFormat.getEncoding() == AudioFormat.Encoding.ALAW))
				nSampleSizeInBits = 16;
			if (nSampleSizeInBits != 8)
				nSampleSizeInBits = 16;
			AudioFormat targetFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(),
					nSampleSizeInBits, sourceFormat.getChannels(),
					sourceFormat.getChannels() * (nSampleSizeInBits / 8), sourceFormat.getSampleRate(), false);
			log.info("Create Line : Target format: " + targetFormat);
			// Keep a reference on encoded stream to progress notification.
			this.encodedAudioInputStream = this.audioInputStream;
			try
			{
				// Get total length in bytes of the encoded stream.
				this.encodedLength = this.encodedAudioInputStream.available();
			}
			catch (IOException e)
			{
				log.error("Cannot get m_encodedaudioInputStream.available()", e);
			}
			// Create decoded stream.
			this.audioInputStream = AudioSystem.getAudioInputStream(targetFormat, this.audioInputStream);
			AudioFormat audioFormat = this.audioInputStream.getFormat();
			DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat, AudioSystem.NOT_SPECIFIED);
			Mixer mixer = getMixer(this.mixerName);
			if (mixer != null)
			{
				log.info("Mixer : " + mixer.getMixerInfo().toString());
				this.sourceDataLine = (SourceDataLine) mixer.getLine(info);
			}
			else
			{
				this.sourceDataLine = (SourceDataLine) AudioSystem.getLine(info);
				this.mixerName = null;
			}
			log.info("Line : " + this.sourceDataLine.toString());
			log.debug("Line Info : " + this.sourceDataLine.getLineInfo().toString());
			log.debug("Line AudioFormat: " + this.sourceDataLine.getFormat().toString());
		}
	}

	/**
	 * Opens the line.
	 */
	protected void openLine() throws LineUnavailableException
	{
		if (this.sourceDataLine != null)
		{
			AudioFormat audioFormat = this.audioInputStream.getFormat();
			int buffersize = this.lineBufferSize;
			if (buffersize <= 0)
				buffersize = this.sourceDataLine.getBufferSize();
			this.lineCurrentBufferSize = buffersize;
			this.sourceDataLine.open(audioFormat, buffersize);
			log.info("Open Line : BufferSize=" + buffersize);
			/*-- Display supported controls --*/
			Control[] c = this.sourceDataLine.getControls();
			for (int p = 0; p < c.length; p++)
			{
				log.debug("Controls : " + c[p].toString());
			}
			/*-- Is Gain Control supported ? --*/
			if (this.sourceDataLine.isControlSupported(FloatControl.Type.MASTER_GAIN))
			{
				this.gainControl = (FloatControl) this.sourceDataLine.getControl(FloatControl.Type.MASTER_GAIN);
				log.info("Master Gain Control : [" + this.gainControl.getMinimum() + ","
						+ this.gainControl.getMaximum() + "] " + this.gainControl.getPrecision());
			}
			/*-- Is Pan control supported ? --*/
			if (this.sourceDataLine.isControlSupported(FloatControl.Type.PAN))
			{
				this.panControl = (FloatControl) this.sourceDataLine.getControl(FloatControl.Type.PAN);
				log.info("Pan Control : [" + this.panControl.getMinimum() + "," + this.panControl.getMaximum() + "] "
						+ this.panControl.getPrecision());
			}
		}
	}

	/**
	 * Stops the playback.<br>
	 * 
	 * Player Status = STOPPED.<br>
	 * Thread should free Audio ressources.
	 */
	protected void stopPlayback()
	{
		if ((this.status == PlayerStatus.PLAYING) || (this.status == PlayerStatus.PAUSED))
		{
			this.status = PlayerStatus.STOPPED;

			int pos = getEncodedStreamPosition();

			if (this.sourceDataLine != null)
			{
				this.sourceDataLine.flush();
				this.sourceDataLine.stop();
			}

			synchronized (this.audioInputStream)
			{
				closeStream();
			}

			notifyEvent(PlayerEvent.STOPPED, pos, -1, null);

			log.info("stopPlayback() completed");
		}
	}

	/**
	 * Pauses the playback.<br>
	 * 
	 * Player Status = PAUSED.
	 */
	protected void pausePlayback()
	{
		if (this.sourceDataLine != null)
		{
			if (this.status == PlayerStatus.PLAYING)
			{
				this.sourceDataLine.flush();
				this.sourceDataLine.stop();
				this.status = PlayerStatus.PAUSED;
				log.info("pausePlayback() completed");
				notifyEvent(PlayerEvent.PAUSED, getEncodedStreamPosition(), -1, null);
			}
		}
	}

	/**
	 * Resumes the playback.<br>
	 * 
	 * Player Status = PLAYING.
	 */
	protected void resumePlayback()
	{
		if (this.sourceDataLine != null)
		{
			if (this.status == PlayerStatus.PAUSED)
			{
				this.sourceDataLine.start();
				this.status = PlayerStatus.PLAYING;
				log.info("resumePlayback() completed");
				notifyEvent(PlayerEvent.RESUMED, getEncodedStreamPosition(), -1, null);
			}
		}
	}

	/**
	 * Starts playback.
	 */
	protected void startPlayback() throws BasicPlayerException
	{
		if (this.status == PlayerStatus.STOPPED)
			initAudioInputStream();
		if (this.status == PlayerStatus.OPENED)
		{
			log.info("startPlayback called");
			if (!(this.thread == null || !this.thread.isAlive()))
			{
				log.info("WARNING: old thread still running!!");
				int cnt = 0;
				while (this.status != PlayerStatus.OPENED)
				{
					try
					{
						if (this.thread != null)
						{
							log.info("Waiting ... " + cnt);
							cnt++;
							Thread.sleep(1000);
							if (cnt > 2)
							{
								this.thread.interrupt();
							}
						}
					}
					catch (InterruptedException e)
					{
						throw new BasicPlayerException(BasicPlayerException.WAITERROR, e);
					}
				}
			}
			// Open SourceDataLine.
			try
			{
				initLine();
			}
			catch (LineUnavailableException e)
			{
				throw new BasicPlayerException(BasicPlayerException.CANNOTINITLINE, e);
			}
			log.info("Creating new thread");
			this.thread = new Thread(this, "BasicPlayer");
			this.thread.start();
			if (this.sourceDataLine != null)
			{
				this.sourceDataLine.start();
				this.status = PlayerStatus.PLAYING;
				notifyEvent(PlayerEvent.PLAYING, getEncodedStreamPosition(), -1, null);
			}
		}
	}

	/**
	 * Main loop.
	 * 
	 * Player Status == STOPPED || SEEKING => End of Thread + Freeing Audio
	 * Ressources.<br>
	 * Player Status == PLAYING => Audio stream data sent to Audio line.<br>
	 * Player Status == PAUSED => Waiting for another status.
	 */
	@SuppressWarnings("unchecked")
	public void run()
	{
		log.info("Thread Running");
		int nBytesRead = 1;
		byte[] abData = new byte[EXTERNAL_BUFFER_SIZE];

		// Lock stream while playing.
		synchronized (this.audioInputStream)
		{
			// Main play/pause loop.
			while ((nBytesRead != -1) && (this.status != PlayerStatus.STOPPED) && (this.status != PlayerStatus.SEEKING)
					&& (this.status != PlayerStatus.UNKNOWN))
			{
				if (this.status == PlayerStatus.PLAYING)
				{
					// Play.
					try
					{
						nBytesRead = this.audioInputStream.read(abData, 0, abData.length);
						if (nBytesRead >= 0)
						{
							byte[] pcm = new byte[nBytesRead];
							System.arraycopy(abData, 0, pcm, 0, nBytesRead);
							if (this.sourceDataLine.available() >= this.sourceDataLine.getBufferSize())
								log.debug("Underrun : " + this.sourceDataLine.available() + "/"
										+ this.sourceDataLine.getBufferSize());

							this.sourceDataLine.write(abData, 0, nBytesRead);
							// Compute position in bytes in encoded stream.
							int nEncodedBytes = getEncodedStreamPosition();
							// Notify listeners

							// ponowne sprawdzenie dla pewnosci
							if (this.status == PlayerStatus.PLAYING)
							{
								for (BasicPlayerListener bpl : this.listeners)
								{
									if (this.audioInputStream instanceof PropertiesContainer)
									{
										// Pass audio parameters such as instant
										// bitrate, ...
										Map<String, Object> properties = ((PropertiesContainer) this.audioInputStream)
												.properties();
										bpl.progress(nEncodedBytes, this.sourceDataLine.getMicrosecondPosition(), pcm,
												properties);
									}
									else
										bpl.progress(nEncodedBytes, this.sourceDataLine.getMicrosecondPosition(), pcm,
												this.emptyMap);
								}
							}
						}
					}
					catch (IOException e)
					{
						log.error("Thread cannot run()", e);
						this.status = PlayerStatus.STOPPED;
						notifyEvent(PlayerEvent.STOPPED, getEncodedStreamPosition(), -1, null, true);
					}
					// Nice CPU usage.
					if (this.threadSleep > 0)
					{
						try
						{
							Thread.sleep(this.threadSleep);
						}
						catch (InterruptedException e)
						{
							log.error("Thread cannot sleep(" + this.threadSleep + ")", e);
						}
					}
				}
				else
				{
					// Pause
					try
					{
						Thread.sleep(1000);
					}
					catch (InterruptedException e)
					{
						log.error("Thread cannot sleep(1000)", e);
					}
				}
			}
			// Free audio resources.
			if (this.sourceDataLine != null)
			{
				this.sourceDataLine.drain();
				this.sourceDataLine.stop();
				this.sourceDataLine.close();
				this.sourceDataLine = null;
			}
			// Notification of "End Of Media"
			if (nBytesRead == -1)
			{
				notifyEvent(PlayerEvent.EOM, getEncodedStreamPosition(), -1, null, true);
				// notifyEvent(PlayerEvent.STOPPED, getEncodedStreamPosition(),
				// -1, null);
			}
			// Close stream.
			closeStream();
		}

		this.status = PlayerStatus.STOPPED;
		// notifyEvent(PlayerEvent.STOPPED, getEncodedStreamPosition(), -1,
		// null);
		log.info("Thread completed");
	}

	/**
	 * Skip bytes in the File inputstream. It will skip N frames matching to
	 * bytes, so it will never skip given bytes length exactly.
	 * 
	 * @param bytes
	 * @return value>0 for File and value=0 for URL and InputStream
	 * @throws BasicPlayerException
	 */
	protected long skipBytes(long bytes) throws BasicPlayerException
	{
		long totalSkipped = 0;
		if (this.dataSource instanceof File)
		{
			log.info("Bytes to skip : " + bytes);
			PlayerStatus previousStatus = this.status;
			this.status = PlayerStatus.SEEKING;
			long skipped = 0;
			try
			{
				synchronized (this.audioInputStream)
				{
					notifyEvent(PlayerEvent.SEEKING, getEncodedStreamPosition(), -1, null);
					initAudioInputStream();
					if (this.audioInputStream != null)
					{
						// Loop until bytes are really skipped.
						while (totalSkipped < (bytes - SKIP_INACCURACY_SIZE))
						{
							skipped = this.audioInputStream.skip(bytes - totalSkipped);
							if (skipped == 0)
								break;
							totalSkipped = totalSkipped + skipped;
							log.info("Skipped : " + totalSkipped + "/" + bytes);
							if (totalSkipped == -1)
								throw new BasicPlayerException(BasicPlayerException.SKIPNOTSUPPORTED);
						}
					}
				}
				notifyEvent(PlayerEvent.SEEKED, getEncodedStreamPosition(), -1, null);
				this.status = PlayerStatus.OPENED;
				if (previousStatus == PlayerStatus.PLAYING)
					startPlayback();
				else if (previousStatus == PlayerStatus.PAUSED)
				{
					startPlayback();
					pausePlayback();
				}
			}
			catch (IOException e)
			{
				throw new BasicPlayerException(e);
			}
		}
		return totalSkipped;
	}

	protected void notifyEvent(PlayerEvent code, int position, double value, Object description)
	{
		notifyEvent(code, position, value, description, false);
	}

	/**
	 * Notify listeners about a BasicPlayerEvent.
	 * 
	 * @param code
	 *            event code.
	 * @param position
	 *            in the stream when the event occurs.
	 */
	protected void notifyEvent(PlayerEvent code, int position, double value, Object description, boolean newThread)
	{
		BasicPlayerEventLauncher trigger = new BasicPlayerEventLauncher(code, position, value, description,
				new ArrayList<BasicPlayerListener>(this.listeners), this);

		if (newThread)
		{
			trigger.start();
		}
		else
		{
			trigger.fire();
		}
	}

	protected int getEncodedStreamPosition()
	{
		int nEncodedBytes = -1;
		if (this.dataSource instanceof File)
		{
			try
			{
				if (this.encodedAudioInputStream != null)
				{
					nEncodedBytes = this.encodedLength - this.encodedAudioInputStream.available();
				}
			}
			catch (IOException e)
			{
				log.debug("Cannot get m_encodedaudioInputStream.available()", e);
			}
		}
		return nEncodedBytes;
	}

	protected void closeStream()
	{
		// Close stream.
		try
		{
			if (this.audioInputStream != null)
			{
				this.audioInputStream.close();
				log.info("Stream closed");
			}
		}
		catch (IOException e)
		{
			log.info("Cannot close stream", e);
		}
	}

	/**
	 * Returns true if Gain control is supported.
	 */
	public boolean hasGainControl()
	{
		if (this.gainControl == null)
		{
			// Try to get Gain control again (to support J2SE 1.5)
			if ((this.sourceDataLine != null)
					&& (this.sourceDataLine.isControlSupported(FloatControl.Type.MASTER_GAIN)))
				this.gainControl = (FloatControl) this.sourceDataLine.getControl(FloatControl.Type.MASTER_GAIN);
		}
		return this.gainControl != null;
	}

	/**
	 * Returns Gain value.
	 */
	public float getGainValue()
	{
		if (hasGainControl())
		{
			return this.gainControl.getValue();
		}
		else
		{
			return 0.0F;
		}
	}

	/**
	 * Gets max Gain value.
	 */
	public float getMaximumGain()
	{
		if (hasGainControl())
		{
			return this.gainControl.getMaximum();
		}
		else
		{
			return 0.0F;
		}
	}

	/**
	 * Gets min Gain value.
	 */
	public float getMinimumGain()
	{
		if (hasGainControl())
		{
			return this.gainControl.getMinimum();
		}
		else
		{
			return 0.0F;
		}
	}

	/**
	 * Returns true if Pan control is supported.
	 */
	public boolean hasPanControl()
	{
		if (this.panControl == null)
		{
			// Try to get Pan control again (to support J2SE 1.5)
			if ((this.sourceDataLine != null) && (this.sourceDataLine.isControlSupported(FloatControl.Type.PAN)))
				this.panControl = (FloatControl) this.sourceDataLine.getControl(FloatControl.Type.PAN);
		}
		return this.panControl != null;
	}

	/**
	 * Returns Pan precision.
	 */
	public float getPrecision()
	{
		if (hasPanControl())
		{
			return this.panControl.getPrecision();
		}
		else
		{
			return 0.0F;
		}
	}

	/**
	 * Returns Pan value.
	 */
	public float getPan()
	{
		if (hasPanControl())
		{
			return this.panControl.getValue();
		}
		else
		{
			return 0.0F;
		}
	}

	/**
	 * @see javazoom.jlgui.basicplayer.BasicController#seek(long)
	 */
	public long seek(long bytes) throws BasicPlayerException
	{
		return skipBytes(bytes);
	}

	/**
	 * @see javazoom.jlgui.basicplayer.BasicController#play()
	 */
	public void play() throws BasicPlayerException
	{
		startPlayback();
	}

	/**
	 * @see javazoom.jlgui.basicplayer.BasicController#stop()
	 */
	public void stop()
	{
		stopPlayback();
	}

	/**
	 * @see javazoom.jlgui.basicplayer.BasicController#pause()
	 */
	public void pause()
	{
		pausePlayback();
	}

	/**
	 * @see javazoom.jlgui.basicplayer.BasicController#resume()
	 */
	public void resume()
	{
		resumePlayback();
	}

	/**
	 * Sets Pan value. Line should be opened before calling this method. Linear
	 * scale : -1.0 <--> +1.0
	 */
	public void setPan(double fPan) throws BasicPlayerException
	{
		if (hasPanControl())
		{
			log.debug("Pan : " + fPan);
			this.panControl.setValue((float) fPan);
			notifyEvent(PlayerEvent.PAN, getEncodedStreamPosition(), fPan, null);
		}
		else
			throw new BasicPlayerException(BasicPlayerException.PANCONTROLNOTSUPPORTED);
	}

	/**
	 * Sets Gain value. Line should be opened before calling this method. Linear
	 * scale 0.0 <--> 1.0 Threshold Coef. : 1/2 to avoid saturation.
	 */
	public void setGain(double fGain) throws BasicPlayerException
	{
		if (hasGainControl())
		{
			double minGainDB = getMinimumGain();
			double ampGainDB = ((10.0f / 20.0f) * getMaximumGain()) - getMinimumGain();
			double cste = Math.log(10.0) / 20;
			double valueDB = minGainDB + (1 / cste) * Math.log(1 + (Math.exp(cste * ampGainDB) - 1) * fGain);
			log.debug("Gain : " + valueDB);
			this.gainControl.setValue((float) valueDB);
			notifyEvent(PlayerEvent.GAIN, getEncodedStreamPosition(), fGain, null);
		}
		else
			throw new BasicPlayerException(BasicPlayerException.GAINCONTROLNOTSUPPORTED);
	}

	public List<String> getMixers()
	{
		List<String> mixers = new ArrayList<String>();
		Mixer.Info[] mInfos = AudioSystem.getMixerInfo();
		if (mInfos != null)
		{
			for (int i = 0; i < mInfos.length; i++)
			{
				Line.Info lineInfo = new Line.Info(SourceDataLine.class);
				Mixer mixer = AudioSystem.getMixer(mInfos[i]);
				if (mixer.isLineSupported(lineInfo))
				{
					mixers.add(mInfos[i].getName());
				}
			}
		}
		return mixers;
	}

	public Mixer getMixer(String name)
	{
		Mixer mixer = null;
		if (name != null)
		{
			Mixer.Info[] mInfos = AudioSystem.getMixerInfo();
			if (mInfos != null)
			{
				for (int i = 0; i < mInfos.length; i++)
				{
					if (mInfos[i].getName().equals(name))
					{
						mixer = AudioSystem.getMixer(mInfos[i]);
						break;
					}
				}
			}
		}
		return mixer;
	}

	public String getMixerName()
	{
		return this.mixerName;
	}

	public void setMixerName(String name)
	{
		this.mixerName = name;
	}
}
